/**
 * License would add here
 */

#ifndef UTILS_JS_VALUE_REF_H
#define UTILS_JS_VALUE_REF_H

#include "js_value_base.h"

class JSFunction;
class JSValueWrapper;

class JSValueRef {
public:
    JSValueRef(napi_env env, napi_value value, uint32_t refCount = 0)
        : env_(env)
        , copied_(new size_t)
    {
        JSValue jsValue(env, value);
        if (!jsValue.IsObject() && !jsValue.IsFunction()) {
            THROW_EXCEPTION("object expected");
        }
        ref_ = CreateReference(env, value, refCount);
        RegistCleanUpHook();
        *copied_ = 0;
    }
    JSValueRef(napi_env env, napi_ref ref)
        : env_(env)
        , ref_(ref)
        , copied_(new size_t)
    {
        *copied_ = 1;
    }
    JSValueRef(const JSValue& value)
        : env_(value.env_)
        , copied_(new size_t)
    {
        if (!value.IsObject() && !value.IsFunction()) {
            THROW_EXCEPTION("object expected");
        }
        ref_ = CreateReference(env_, value, 0);
        RegistCleanUpHook();
        *copied_ = 0;
    }
    JSValueRef(const JSValueRef& ref)
        : env_(ref.env_)
        , ref_(ref.ref_)
        , copied_(ref.copied_)
    {
        copied_++;
    }
    JSValueRef(const JSValue& value, uint32_t refCount)
        : env_(value.env_)
        , copied_(new size_t)
    {
        if (!value.IsObject() && !value.IsFunction()) {
            THROW_EXCEPTION("object expected");
        }
        ref_ = CreateReference(env_, value, refCount);
        RegistCleanUpHook();
        *copied_ = 0;
    }
    JSValueRef(const JSFunction& value);

    ~JSValueRef()
    {
        napi_remove_env_cleanup_hook(env_, CleanUp, this);
        if (*copied_ == 0) {
            napi_delete_reference(env_, ref_);
        } else {
            copied_--;
        }
        ref_ = nullptr;
    }

    inline JSValue GetValue() const
    {
        return GetReferenceValue(env_, ref_);
    }

    inline napi_ref* operator&() { return &ref_; }
    inline void operator=(napi_ref ref) { ref_ = ref; };
    inline operator napi_ref() const { return ref_; }
    inline operator napi_value() const { return GetValue(); }
    inline operator JSValue() const { return JSValue(env_, GetValue()); }

    inline uint32_t Ref()
    {
        uint32_t count = 0;
        napi_reference_ref(env_, ref_, &count);
        return count;
    }

    inline uint32_t Unref()
    {
        uint32_t count = 0;
        napi_reference_unref(env_, ref_, &count);
        return count;
    }

    inline JSValue ToJSValue() { return JSValue(env_, GetValue()); }

    inline static napi_ref CreateReference(napi_env env,
        napi_value value,
        uint32_t initCount = 0)
    {
        napi_ref ref = nullptr;
        napi_create_reference(env, value, initCount, &ref);
        return ref;
    }

    inline static JSValue GetReferenceValue(napi_env env, napi_ref ref)
    {
        JSValue result(env);
        napi_get_reference_value(env, ref, &result);
        result.UpdateType();
        return result;
    }

protected:
    JSValueRef(napi_env env)
        : env_(env) {};

private:
    static void CleanUp(void* data)
    {
        delete reinterpret_cast<JSValueRef*>(data);
    }

    void RegistCleanUpHook()
    {
        napi_add_env_cleanup_hook(env_, CleanUp, this);
    }

    const napi_env env_;
    napi_ref ref_;
    size_t *copied_ {nullptr};

    friend class JSValueWrapper;
};

#endif /* UTILS_JS_VALUE_REF_H */